tool
extends Spatial

class_name CuboidPolyhedron

var poly: Spatial = null

export(String) var object_type_id: String

enum PolyhedronFlags {
	DISCARDABLE = 0x1, # It can be killed without losing the game.
	MAIN = 0x2, # Main block, used in winning condition (usually game wins when all main blocks go to the exit), and game logic (only main blocks can go to the exit and eat checkpoints).
	FRAGILE = 0x4, # It will be killed when falling (in other words it can't fall).
	SUPPORTER = 0x8, # It can support other polyhedra.
	TILTABLE = 0x10, # It can be tilted (experimental, buggy).
	TILT_SUPPORTER = 0x20, # It can support other tilting polyhedra.
	VISIBLE = 0x40, # It is visible and take part in the game logic (only an ad-hoc solution).
	TARGET = 0x80, # It is a target block, e.g. in Sokoban.
	EXIT = 0x100, # It is an exit block (e.g. an exit which checks shape).
	CONTINUOUS_HITTEST = 0x200, # Enables continuous hit test.
}

export(int, FLAGS,
	"DISCARDABLE",
	"MAIN",
	"FRAGILE",
	"SUPPORTER",
	"TILTABLE",
	"TILT_SUPPORTER",
	"VISIBLE",
	"TARGET",
	"EXIT",
	"CONTINUOUS_HITTEST") var flags: int = \
	PolyhedronFlags.MAIN | PolyhedronFlags.FRAGILE | PolyhedronFlags.SUPPORTER | \
	PolyhedronFlags.VISIBLE

enum PolyhedronSupportType {
	ALL, # All (bottom) blocks of it must be supported.
	HALF, # It is stable if at least half of its (bottom) block is supported.
	ANY, # It is stable if any one of its (bottom) block is supported (e.g. blocks in PuzzleBoy).
	SPANNABLE, # It is stable if the bounding box equals the whole rect.
	FLOATING, # It isn't affected by gravity, e.g. the target block in PuzzleBoy.
}

export(PolyhedronSupportType) var support_type: int = PolyhedronSupportType.SPANNABLE

# Note: in Godot, left/right <-> Z axis, up/down <-> X axis
enum PolyhedronMovement {
	ROLLING_HORZ = 0x1, # Rolling left/right (<-> rotating around X axis)
	ROLLING_VERT = 0x2, # Rolling up/down (<-> rotating around Z axis)
	MOVING_Z = 0x4, # Moving around Z axis
	MOVING_X = 0x8, # Moving around X axis
	MOVING_Y = 0x10, # Moving around Y axis
	ROTATING_Z = 0x20, # Rotating around (global) Z axis
	ROTATING_X = 0x40, # Rotating around (global) X axis
	ROTATING_Y = 0x80, # Rotating around (global) Y axis
	ROTATING_LOCAL_Z = 0x100, # Rotating around local Z axis
	ROTATING_LOCAL_X = 0x200, # Rotating around local X axis
	ROTATING_LOCAL_Y = 0x400, # Rotating around local Y axis
	ROTATING_LOCAL_MASK = 0x700,
}

export(int, FLAGS,
	"ROLLING_HORZ",
	"ROLLING_VERT",
	"MOVING_Z",
	"MOVING_X",
	"MOVING_Y",
	"ROTATING_Z",
	"ROTATING_X",
	"ROTATING_Y",
	"ROTATING_LOCAL_Z",
	"ROTATING_LOCAL_X",
	"ROTATING_LOCAL_Y") var movement: int = \
	PolyhedronMovement.ROLLING_HORZ | PolyhedronMovement.ROLLING_VERT

const LOCAL_ROT_AXES = [
	PolyhedronMovement.ROTATING_LOCAL_X,
	PolyhedronMovement.ROTATING_LOCAL_Y,
	PolyhedronMovement.ROTATING_LOCAL_Z,
]

enum PolyhedronController {
	PASSIVE, # It can only be pushed by player
	PLAYER, # It can only be controlled by player
	ELEVATOR, # It can only be controlled when player is standing on it
}

export(PolyhedronController) var controller: int = PolyhedronController.PLAYER

export(Dictionary) var map_position: Dictionary = PolyhedronPosition.DEFAULT setget set_map_position

export(PackedScene) var appearance_override: PackedScene
export(PackedScene) var appearance_override_solid: PackedScene
export(PackedScene) var appearance_override_antenna: PackedScene

export(Material) var material_override: Material setget set_material_override
var material_override_selected: Material = null
var material_override_flash: Material = null

var map_data: MapData = null

# Vector3 -> PolyhedronCellType (a.k.a int)
var data: Dictionary = {}
# Vector3 -> Spatial
var instances: Dictionary = {}
var lbound: Vector3 = Vector3()
var ubound: Vector3 = Vector3()

enum PolyhedronCellType {
	EMPTY, # empty
	SOLID, # solid block
	ANTENNA, # antenna, don't need to be supported
}

const PolyhedronCellMap: Dictionary = {
	"": PolyhedronCellType.EMPTY,
	"SOLID": PolyhedronCellType.SOLID,
	"ANTENNA": PolyhedronCellType.ANTENNA,
}

const PolyhedronCellInvMap: Dictionary = {
	PolyhedronCellType.EMPTY: "",
	PolyhedronCellType.SOLID: "SOLID",
	PolyhedronCellType.ANTENNA: "ANTENNA",
}

var configuration_warning: String = ""

var tween: Tween = null

# note: this is without transform of map_data
var old_transform: Transform
var anim: Dictionary = {}

var selected: bool = false setget set_selected

var hittest_inst: Spatial = null
# Vector3 -> Area
var hittest_objs: Dictionary = {}

enum HitTestReason {
	HITTEST_VALID,
	HITTEST_FALL,
	HITTEST_BLOCKED,
}

signal input_event(camera, event, click_position, click_normal, shape_idx)

func _get_configuration_warning():
	return configuration_warning

func _get_property_list():
	var prop_list: Array = []

	prop_list.append({
		name = "data",
		type = TYPE_DICTIONARY,
		usage = PROPERTY_USAGE_NOEDITOR,
	})

	return prop_list

func _ready():
	recreate()

func process_move_input(input: Vector3, force: bool = false):
	# FIXME: this is in fact a Level
	# workaround for https://github.com/godotengine/godot/issues/21461
	var parent: Spatial = get_parent_spatial()
	if parent == null:
		return

	var type: int = -1
	var dir: int = -1

	if abs(input.z) > 0.5:
		# horizontal
		dir = PolyhedronPosition.HORZ_POS if input.z > 0 else PolyhedronPosition.HORZ_NEG
		if movement & PolyhedronMovement.ROLLING_HORZ:
			type = PolyhedronPosition.ROLL
		elif movement & PolyhedronMovement.MOVING_Z:
			type = PolyhedronPosition.MOVE
		elif movement & PolyhedronMovement.ROTATING_X:
			type = PolyhedronPosition.ROTATE
		elif movement & PolyhedronMovement.ROTATING_LOCAL_MASK:
			var fl: int = map_position.get("flags", PolyhedronPosition.ROT_XYZ)
			fl = (fl >> 3) & 3
			if movement & LOCAL_ROT_AXES[fl]:
				type = PolyhedronPosition.ROTATE
	elif abs(input.x) > 0.5:
		# vertical
		dir = PolyhedronPosition.VERT_POS if input.x > 0 else PolyhedronPosition.VERT_NEG
		if movement & PolyhedronMovement.ROLLING_VERT:
			type = PolyhedronPosition.ROLL
		elif movement & PolyhedronMovement.MOVING_X:
			type = PolyhedronPosition.MOVE
		elif movement & PolyhedronMovement.ROTATING_Z:
			type = PolyhedronPosition.ROTATE
		elif movement & PolyhedronMovement.ROTATING_LOCAL_MASK:
			var fl: int = map_position.get("flags", PolyhedronPosition.ROT_XYZ)
			fl = (fl >> 7) & 3
			if movement & LOCAL_ROT_AXES[fl]:
				type = PolyhedronPosition.ROTATE
	elif abs(input.y) > 0.5:
		dir = PolyhedronPosition.Y_POS if input.y > 0 else PolyhedronPosition.Y_NEG
		if movement & PolyhedronMovement.MOVING_Y:
			type = PolyhedronPosition.MOVE
		elif movement & PolyhedronMovement.ROTATING_Y:
			type = PolyhedronPosition.ROTATE
		elif movement & PolyhedronMovement.ROTATING_LOCAL_MASK:
			var fl: int = map_position.get("flags", PolyhedronPosition.ROT_XYZ)
			fl = (fl >> 5) & 3
			if movement & LOCAL_ROT_AXES[fl]:
				type = PolyhedronPosition.ROTATE

	if type >= 0 and dir >= 0:
		var old_position: Dictionary = map_position
		var new_anim: Dictionary = {}
		var new_position: Dictionary = PolyhedronPosition.move(map_position, {
			"lbound": lbound,
			"ubound": ubound,
			"type": type,
			"dir": dir,
		}, new_anim)

		# check if new position is valid
		var hittest_result: Dictionary = hittest(new_position)
		var valid: bool = false
		if force:
			valid = hittest_result.get("reason", HitTestReason.HITTEST_BLOCKED) != HitTestReason.HITTEST_BLOCKED
		else:
			valid = hittest_result.get("valid", false)

		var flash_polyhedron: CuboidPolyhedron = null

		if valid and not force:
			# check if we make other polyhedron break
			var old_polyhedra_hittest_result: Dictionary = {}
			for poly in parent.polyhedra:
				if poly != self:
					var d: Dictionary = poly.hittest()
					old_polyhedra_hittest_result[poly] = d.get("valid", false)

			# update our position temporarily
			map_position = new_position

			# check hit test result again
			for poly in parent.polyhedra:
				if poly != self and old_polyhedra_hittest_result[poly]:
					var d: Dictionary = poly.hittest()
					if not d.get("valid", false):
						valid = false
						flash_polyhedron = poly
						break

			# restore our position
			map_position = old_position

		if valid:
			old_transform = Transform(PolyhedronPosition.get_basis( \
				map_position.get("flags", PolyhedronPosition.ROT_XYZ)), \
				map_position.get("position", Vector3(0, 0, 0)))
			map_position = new_position
			anim = new_anim

			if tween != null:
				# with animation
# warning-ignore:return_value_discarded
				tween.interpolate_method(self, "update_animation_timer", 0.0, 1.0, 0.3)
# warning-ignore:return_value_discarded
				tween.start()
			else:
				# without animation
				update_position()
		else:
			# show some invalid animations
			var bp: Array = hittest_result.get("blocked_polyhedron", [])
			if not bp.empty():
				flash_polyhedron = bp[0]
			if flash_polyhedron != null:
				if flash_polyhedron.tween != null:
					flash_polyhedron.anim = {"flash": "red"}
# warning-ignore:return_value_discarded
					flash_polyhedron.tween.interpolate_method(flash_polyhedron, "update_animation_timer", 0.0, 1.0, 0.4)
# warning-ignore:return_value_discarded
					flash_polyhedron.tween.start()
			elif map_data != null and tween != null:
				bp = hittest_result.get("blocked_position", [])
				if bp.empty():
					bp = hittest_result.get("unsupported_position", [])
				if not bp.empty():
					map_data.add_flash_animation(bp[0], \
						{"flash": "red", "time": 0.4})

func update_animation_timer(t: float):
	if Engine.editor_hint:
		return

	if anim.empty():
		return

	if map_data == null:
		return

	match anim:
		{"move_axis": var _v, ..}:
			var v: Vector3 = _v as Vector3

			var tf: Transform = old_transform
			tf.origin += v * t
			transform = map_data.transform * tf

			if t < 0.9999:
				return

		{"rot_center": var _c, "rot_axis": var _v, ..}:
			var c: Vector3 = _c as Vector3
			var v: Vector3 = _v as Vector3

			var tf: Transform = old_transform
			tf.origin -= c
			tf = tf.rotated(v, t * PI * 0.5)
			tf.origin += c
			transform = map_data.transform * tf

			if t < 0.9999:
				return

		{"flash": var _c, ..}:
			if t < 0.9999:
				if material_override != null and material_override_flash == null:
					material_override_flash = material_override.duplicate()
					material_override_flash.next_pass = preload("res://materials/highlight2.tres")
				_set_material_override(poly, material_override_flash)

				if material_override_flash != null:
					var np: SpatialMaterial = material_override_flash.next_pass
					match _c:
						"red":
							var f: float = fmod(t * 4.0, 2.0)
							f = min(f, 2.0 - f) * 0.4
							np.albedo_color = Color(1.0, 0.0, 0.0, f)

				return

			material_override_flash = null
			_set_material_override(poly)

	anim.clear()
	update_position()

func recalculate_bound():
	lbound = Vector3()
	ubound = Vector3()

	if not data.empty():
		lbound = Vector3(1e+6, 1e+6, 1e+6)
		ubound = Vector3(-1e+6, -1e+6, -1e+6)
		for o in data:
			var v: Vector3 = o
			var poly_type: int = data[v]
			if poly_type > 0 and poly_type < PolyhedronCellType.size():
				lbound.x = min(lbound.x, v.x)
				lbound.y = min(lbound.y, v.y)
				lbound.z = min(lbound.z, v.z)
				ubound.x = max(ubound.x, v.x)
				ubound.y = max(ubound.y, v.y)
				ubound.z = max(ubound.z, v.z)

func recreate():
	if not Engine.editor_hint:
		if hittest_inst != null:
			hittest_inst.queue_free()
			hittest_inst = null
		hittest_objs.clear()
		hittest_inst = Spatial.new()
		add_child(hittest_inst)

	recalculate_bound()

	if poly != null:
		poly.queue_free()
		poly = null
	instances.clear()
	poly = Spatial.new()
	add_child(poly)

	if not Engine.editor_hint and appearance_override != null:
		var inst: Spatial = appearance_override.instance()
		if inst != null:
			_set_material_override(inst)
			poly.add_child(inst)

	if not data.empty():
		for o in data:
			var v: Vector3 = o
			var poly_type: int = data[v]
			if poly_type > 0 and poly_type < PolyhedronCellType.size():
				var inst: Spatial = create_poly_instance(poly_type)
				if inst != null:
					inst.translate(v)
					poly.add_child(inst)
					instances[v] = inst

				if not Engine.editor_hint:
					var area: Area = preload("res://DebugHitTestArea.tscn").instance()
					area.transform.origin = v
# warning-ignore:return_value_discarded
					area.connect("input_event", self, "_on_input_event")
					hittest_objs[v] = area
					hittest_inst.add_child(area)

	update_position()

func _on_input_event(camera: Node, event: InputEvent, click_position: Vector3, click_normal: Vector3, shape_idx: int):
	emit_signal("input_event", camera, event, click_position, click_normal, shape_idx)

func create_poly_instance(tile: int) -> Spatial:
	if not Engine.editor_hint and appearance_override != null:
		return null

	var ps: PackedScene = null

	match tile:
		PolyhedronCellType.SOLID:
			if not Engine.editor_hint and appearance_override_solid != null:
				ps = appearance_override_solid
			else:
				ps = preload("res://tiles/SolidCuboidPolyhedron.tscn")
		PolyhedronCellType.ANTENNA:
			if not Engine.editor_hint and appearance_override_antenna != null:
				ps = appearance_override_antenna
			else:
				ps = preload("res://tiles/AntennaCuboidPolyhedron.tscn")

	if ps != null:
		var inst: Spatial = ps.instance()
		if inst != null:
			_set_material_override(inst)
			return inst

	return null

func update_position():
	var map_data_name: String = map_position.get("map_data_name", "")
	var position: Vector3 = map_position.get("position", Vector3(0, 0, 0))
	var fl: int = map_position.get("flags", PolyhedronPosition.ROT_XYZ)

	map_data = null

	if map_data_name != "":
		var parent = get_parent_spatial()
		if parent != null and parent is Spatial:
			var node = parent.get_node_or_null(map_data_name)
			if node != null and node is MapData:
				map_data = node

	configuration_warning = ""

	if map_data == null:
		var err_text: String = "MapData with name '%s' not found" % map_data_name
		if Engine.editor_hint:
			configuration_warning = err_text
		else:
			printerr("[CuboidPolyhedron::update_position] ERROR: " + err_text)
	else:
		if not fl in PolyhedronPosition.allPossibleFlagsForCuboid:
			var err_text: String = "MapPosition flag '%d' is invalid" % fl
			if Engine.editor_hint:
				configuration_warning = err_text
			else:
				printerr("[CuboidPolyhedron::update_position] ERROR: " + err_text)
		transform = map_data.transform * \
			Transform(PolyhedronPosition.get_basis(fl), position)

	update_configuration_warning()

func set_map_position(p_position):
	map_position = p_position

	if Engine.editor_hint:
		update_position()

func get_tile(position: Vector3) -> int:
	return data.get(position, PolyhedronCellType.EMPTY)

func get_tile_id(position: Vector3) -> String:
	return PolyhedronCellInvMap.get( \
		data.get(position, PolyhedronCellType.EMPTY), "")

func set_tile(position: Vector3, tile: int) -> void:
	# remove instance
	# TODO: animation
	var current_inst: Spatial = instances.get(position, null)
	if current_inst != null:
# warning-ignore:return_value_discarded
		instances.erase(position)
		current_inst.queue_free()

	# remove hit test area
	if not Engine.editor_hint:
		var obj: Spatial = hittest_objs.get(position, null)
		if obj != null:
# warning-ignore:return_value_discarded
			hittest_objs.erase(position)
			obj.queue_free()

	# remove data
# warning-ignore:return_value_discarded
	data.erase(position)

	if tile > 0 and tile < PolyhedronCellType.size():
		# update bound
		lbound.x = min(lbound.x, position.x)
		lbound.y = min(lbound.y, position.y)
		lbound.z = min(lbound.z, position.z)
		ubound.x = max(ubound.x, position.x)
		ubound.y = max(ubound.y, position.y)
		ubound.z = max(ubound.z, position.z)

		# add new data
		data[position] = tile

		if poly == null:
			printerr("[CuboidPolyhedron::set_tile] poly node is empty, skip creating instance")
			return

		# add instance
		# TODO: animation
		var inst: Spatial = create_poly_instance(tile)
		if inst != null:
			inst.transform.origin = position
			poly.add_child(inst)
			instances[position] = inst

		# add hit test area
		if not Engine.editor_hint:
			var area: Area = preload("res://DebugHitTestArea.tscn").instance()
			area.transform.origin = position
# warning-ignore:return_value_discarded
			area.connect("input_event", self, "_on_input_event")
			hittest_objs[position] = area
			hittest_inst.add_child(area)

	else:
		# recalculate bound is necessary, otherwise game logic will break
		if not Engine.editor_hint:
			recalculate_bound()

func set_tile_by_id(position: Vector3, tile_id: String) -> void:
	set_tile(position, PolyhedronCellMap.get(tile_id, PolyhedronCellType.EMPTY))

func _set_material_override(obj: Spatial, mat = null):
	if Engine.editor_hint or obj == null:
		return
	if obj is GeometryInstance and obj.get_layer_mask_bit(1):
		if mat == null:
			if selected:
				if material_override != null and material_override_selected == null:
					material_override_selected = material_override.duplicate()
					material_override_selected.next_pass = preload("res://materials/highlight_material.tres")
				mat = material_override_selected
			else:
				mat = material_override
		obj.material_override = mat
	for o in obj.get_children():
		_set_material_override(o, mat)

func set_material_override(mat: Material):
	if material_override != mat:
		material_override = mat
		material_override_selected = null
		_set_material_override(poly)

func set_selected(p_selected: bool):
	if selected != p_selected:
		selected = p_selected
		_set_material_override(poly)

func is_hittest_object() -> bool:
	return visible and \
	(flags & PolyhedronFlags.VISIBLE) != 0

func is_blocked_object() -> bool:
	return visible and \
	(flags & PolyhedronFlags.VISIBLE) != 0 and \
	(flags & PolyhedronFlags.EXIT) == 0

func is_selectable() -> bool:
	return visible and \
	(flags & PolyhedronFlags.VISIBLE) != 0 and \
	(flags & PolyhedronFlags.EXIT) == 0 and \
	controller == PolyhedronController.PLAYER

func hittest(pp = null) -> Dictionary:
	# FIXME: this is in fact a Level
	# workaround for https://github.com/godotengine/godot/issues/21461
	var parent: Spatial = get_parent_spatial()
	if parent == null:
		return {}

	if not visible or \
	(flags & PolyhedronFlags.VISIBLE) == 0 or \
	(flags & PolyhedronFlags.EXIT) != 0:
		return {}

	if pp == null:
		pp = map_position.duplicate()

	var md: MapData = parent.get_node_or_null(pp.get("map_data_name", "")) as MapData

	var reason: int = HitTestReason.HITTEST_VALID

	# the tiles which support this polyhedron
	# [MapData, Vector3] -> Dictionary (return value of Level::get_tile)
	var supporter_position: Dictionary = {}
	# the polyhedra which support this polyhedron.
	var supporter_polyhedron: Array = []
	# the hit-tested tiles.
	# [MapData, Vector3] -> Dictionary (return value of Level::get_tile)
	var hittest_position: Dictionary = {}

	# array of Vector3
	var blocked_position: Array = []
	# array of polyhedron
	var blocked_polyhedron: Array = []

	# array of Vector3
	var unsupported_position: Array = []

	var min_y: float = 1e+6
	var bounding_rect: Rect2 = Rect2(1e+6, 1e+6, -2e+6, -2e+6)
	var supported_bounding_rect: Rect2 = Rect2(1e+6, 1e+6, -2e+6, -2e+6)
	var number_of_bottom_tiles: int = 0
	var number_of_bottom_solid_blocks: int = 0
	var number_of_supported_solid_blocks: int = 0

	var is_blocked: bool = false

	# loop through tiles of current polyhedron
	for o in data:
		var v: Vector3 = o as Vector3
		var tile: int = data[v]
		if tile > 0:
			var global_v: Vector3 = PolyhedronPosition.map_local_to_global(pp, v)

			# update bounding rect
			bounding_rect = bounding_rect.expand(Vector2(global_v.x, global_v.z))

			# add hittest position
			var key: Array = [md, global_v]
			hittest_position[key] = parent.get_tile(md, global_v, [self])

			# check if it's blocked
			var ba: Array = parent.get_blocked_area(md, global_v, [self])
			if not ba.empty():
				is_blocked = true

				blocked_position.push_back(global_v)

				for d in ba:
					if d is Dictionary:
						match d:
							{"polyhedron": var _p, ..}:
								if not _p in blocked_polyhedron:
									blocked_polyhedron.push_back(_p)

			# if bottom is changed
			if global_v.y < min_y:
				min_y = global_v.y

				supporter_position.clear()
				supporter_polyhedron.clear()
				unsupported_position.clear()

				supported_bounding_rect = Rect2(1e+6, 1e+6, -2e+6, -2e+6)
				number_of_bottom_tiles = 0
				number_of_bottom_solid_blocks = 0
				number_of_supported_solid_blocks = 0

			# if current tile is at bottom
			if global_v.y == min_y:
				number_of_bottom_tiles += 1
				if tile == PolyhedronCellType.SOLID:
					number_of_bottom_solid_blocks += 1

					# check if it's supported
					var current_supp = null
					var global_v2: Vector3 = Vector3(global_v.x, global_v.y - 1, global_v.z)

					ba = parent.get_blocked_area(md, global_v2, [self])

					for obj in ba:
						var d: Dictionary = {}

						if obj is Vector3:
							# a tile in map data
							d = parent.get_tile(md, obj, [self])
						elif obj is Dictionary:
							# a tile in a polyhedron
							d = obj

						match d:
							{"map_data", "tile": var _tt, ..}:
								var tt: TileType = _tt as TileType
								if (tt.flags & TileType.TileFlags.SUPPORTER) != 0:
									current_supp = d
							{"polyhedron": var _p, "tile": var _t, ..}:
								if (_p.flags & PolyhedronFlags.SUPPORTER) != 0 and \
								_t == PolyhedronCellType.SOLID:
									current_supp = d
									if not _p in supporter_polyhedron:
										supporter_polyhedron.push_back(_p)

						if current_supp != null:
							break

					if current_supp != null:
						# it's supported
						key = [md, global_v2]
						supporter_position[key] = current_supp
						number_of_supported_solid_blocks += 1
						supported_bounding_rect = supported_bounding_rect.expand(Vector2(global_v.x, global_v.z))
					else:
						# it's not supported
						unsupported_position.push_back(global_v2)

	# calculate hittest result
	if is_blocked:
		reason = HitTestReason.HITTEST_BLOCKED
	elif support_type != PolyhedronSupportType.FLOATING and number_of_supported_solid_blocks == 0:
		reason = HitTestReason.HITTEST_FALL
	else:
		match support_type:
			PolyhedronSupportType.ALL:
				if number_of_supported_solid_blocks < number_of_bottom_solid_blocks:
					reason = HitTestReason.HITTEST_FALL
			PolyhedronSupportType.HALF:
				if number_of_supported_solid_blocks * 2 < number_of_bottom_solid_blocks:
					reason = HitTestReason.HITTEST_FALL
			PolyhedronSupportType.SPANNABLE:
				if bounding_rect != supported_bounding_rect:
					reason = HitTestReason.HITTEST_FALL

				# update unsupported position
				var up: Array = []
				for o in unsupported_position:
					var v: Vector3 = o
					if v.x < supported_bounding_rect.position.x or \
					v.x > supported_bounding_rect.end.x or \
					v.z < supported_bounding_rect.position.y or \
					v.z > supported_bounding_rect.end.y:
						up.push_back(v)

				unsupported_position = up

	# check if the result is valid
	var valid: bool = false
	match reason:
		HitTestReason.HITTEST_VALID:
			valid = true
		HitTestReason.HITTEST_FALL:
			if (flags & PolyhedronFlags.DISCARDABLE) != 0 or \
			(flags & PolyhedronFlags.FRAGILE) == 0:
				valid = true

	return {
		"valid": valid,
		"reason": reason,
		"supporter_position": supporter_position,
		"supporter_polyhedron": supporter_polyhedron,
		"hittest_position": hittest_position,
		"min_y": min_y,
		"bounding_rect": bounding_rect,
		"supported_bounding_rect": supported_bounding_rect,
		"number_of_bottom_tiles": number_of_bottom_tiles,
		"number_of_bottom_solid_blocks": number_of_bottom_solid_blocks,
		"number_of_supported_solid_blocks": number_of_supported_solid_blocks,
		"blocked_position": blocked_position,
		"blocked_polyhedron": blocked_polyhedron,
		"unsupported_position": unsupported_position,
	}
